@@ -11,183 +11,459 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule
# meta fhsdesc: tool, tools, github, info, inline
from . . import loader , utils
from . . inline import InlineCall
import logging
import json
import re
import asyncio
import urllib . request
import json
from datetime import datetime , timedelta
@loader.tds
class GitHubInfoMod ( loader . Module ) :
""" GitHub user info, recent activity and contribution graph """
""" GitHub user information """
strings = {
" name " : " GitHubInfo " ,
" no_username " : " ❗ Provide a GitHub username. " ,
" user_not_found " : " 🚫 User not found: <b> {} </b> " ,
" profile " : " Profile " ,
" api_error " : " ⚠ GitHub API error: <b> {msg} </b> " ,
" no_activity " : " 🕸 No recent activity from <b> {} </b> " ,
" no_contrib " : " 📭 No contribution data for <b> {} </b> " ,
" info_text " : (
" 👤 <b> {name} </b> | <a href= \" {url} \" > {profile} </a> \n "
" 🏢 {company} | 📍 {location} \n "
" 📝 {bio} \n \ n "
" 📦 Repos: <b> {repos} </b> | "
" 👥 Followers: <b> {followers} </b> | "
" no_contrib " : " 📭 No contribution data. " ,
" no_repos " : " 📭 No public repositories. " ,
" no_orgs " : " 📭 Not a member of any organizations. " ,
" no_title " : " No title " ,
" no_desc " : " No descriptio n" ,
" not_specified " : " Not specified " ,
" more_commits " : " ... and {} more \n " ,
" hireable_yes " : " Yes " ,
" hireable_no " : " No " ,
" menu_text " : " Choose a section: " ,
" btn_activity " : " 🔥 Activity " ,
" btn_contrib " : " 📊 Contributions " ,
" btn_repos " : " 📦 Repositories " ,
" btn_orgs " : " 🏛 Organizations " ,
" btn_back " : " ← Back to profile " ,
" profile_header " : " <b>Profile</b> <a href= \" {url} \" > {username} </a> \n \n " ,
" profile_text " : (
" 👤 Name: <b> {name} </b> \n "
" 🏷 Login: <code> {login} </code> \n "
" 📝 Bio: {bio} \n "
" 🏢 Company: {company} \n "
" 📍 Location: {location} \n "
" 📧 Email: {email} \n "
" 🔗 Website: {blog} \n "
" 🐦 Twitter: {twitter} \n "
" 💼 Hireable: {hireable} \n "
" 📊 Type: {type} \n "
" 📦 Public repos: <b> {repos} </b> \n "
" ⭐ Public gists: <b> {gists} </b> \n "
" 👥 Followers: <b> {followers} </b> \n "
" 👣 Following: <b> {following} </b> \n "
" 🕒 Created: <code>{created} </code> "
" 🕐 Created: <code>{created} </code> \n "
" 🕐 Updated: <code> {updated} </code> "
) ,
" activity_header " : " <b>Recent activity:</b> \n " ,
" activity_commit " : " 🔨 {count} commit(s) → <code> {branch} </code> in {repo} " ,
" activity_create " : " ✨ Created {ref_type} in {repo} " ,
" activity_p r" : " 🔄 {action} PR: {title} " ,
" activity_issue " : " ❗ {action} issue: {title} " ,
" activity_star " : " ⭐ Starred {repo} " ,
" activity_fork " : " ⑂ Forked to {fork} " ,
" activity_other " : " ⚡ {event} in {repo} " ,
" contrib_header " : " <b>Contribution graph</b> for <a href=\" https://github.com/ {username } \" > {username } </a>: \n " ,
" contrib_footer " : " ⬛ = 0, 🟩 = 1+ contributions " ,
" activity_header " : " <b>Recent activity</b> <a href= \" https://github.com/ {username} \" > {username} </a> \n \n " ,
" push_heade r" : " 🔨 Pushed to <code> {branch} </code> → <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" push_no_commits " : " 🔨 Pushed (no details) to <code> {branch} </code> → <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" commit_line " : " • <a href= \" {url} \" ><code> {sha} </code></a>: {message} \n " ,
" create_branch " : " ✨ Created branch <code> {ref} </code> in <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" create_tag " : " ✨ Created tag <code> {ref} </code> in <a href=\" https://github.com/ {repo} /releases/tag/ {ref } \" > {repo } </a> \n " ,
" create_repo " : " ✨ Created repository <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" pr_opened " : " 🔄 Opened PR <a href= \" {url} \" ># {} {title} </a> \n " ,
" pr_closed " : " 🔄 Closed PR <a href= \" {url} \" ># {} {title} </a> \n " ,
" pr_merged " : " 🔄 Merged PR <a href= \" {url} \" ># {} {title} </a> \n " ,
" issue_opened " : " ❗ Opened issue <a href= \" {url} \" ># {} {title} </a> \n " ,
" issue_closed " : " ❗ Closed issue <a href= \" {url} \" ># {} {title} </a> \n " ,
" star " : " ⭐ Starred <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" fork " : " ⑂ Forked <a href= \" https://github.com/ {fork} \" > {fork} </a> \n " ,
" other " : " ⚡ {event} in <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" repos_header " : " <b>Top repositories by stars</b> <a href= \" https://github.com/ {username} \" > {username} </a> \n \n " ,
" repo_line " : " ⭐ <b> {stars} </b> | <a href= \" {url} \" > {name} </a> — {desc} \n Language: {lang} | Forks: {forks} \n \n " ,
" orgs_header " : " <b>Organizations</b> <a href= \" https://github.com/ {username} \" > {username} </a> \n \n " ,
" org_line " : " • <a href= \" {url} \" > {login} </a> — {desc} \n " ,
" contrib_header " : " <b>Contribution graph (last year)</b> <a href= \" https://github.com/ {username} \" > {username} </a> \n " ,
" contrib_footer " : " \n ⬛ = 0, 🟩 = 1+ contributions " ,
}
strings_ru = {
" no_username " : " ❗ Укажи имя пользователя GitHub. " ,
" _cls_doc " : " Информация о GitHub- пользователе " ,
" no_username " : " ❗ Укажи GitHub username. " ,
" user_not_found " : " 🚫 Пользователь не найден: <b> {} </b> " ,
" profile " : " Профиль " ,
" no_activity " : " 🕸 Нет активности у <b> {} </b> " ,
" no_contrib " : " 📭 Нет данных о вкладах <b> {} </b> " ,
" info_text " : (
" 👤 <b> {name} </b> | <a href= \" {url} \" > {profile} </a> \n "
" 🏢 {company} | 📍 {location} \n "
" 📝 {bio} \n \n "
" 📦 Репозитории: <b> {repos} </b> | "
" 👥 Подписчики: <b> {followers} </b> | "
" api_error " : " ⚠ Ошибка GitHub API: <b> {msg} </b> " ,
" no_activity " : " 🕸 Нет недавней активности у <b> {} </b> " ,
" no_contrib " : " 📭 Нет данных о контрибуциях. " ,
" no_repos " : " 📭 Нет публичных репозиториев. " ,
" no_orgs " : " 📭 Н е состоит в организациях. " ,
" no_title " : " Без названия " ,
" no_desc " : " Без описания " ,
" not_specified " : " Н е указано" ,
" more_commits " : " ... и ещё {} \n " ,
" hireable_yes " : " Да " ,
" hireable_no " : " Нет " ,
" menu_text " : " Выбери раздел: " ,
" btn_activity " : " 🔥 Активность " ,
" btn_contrib " : " 📊 Контрибы " ,
" btn_repos " : " 📦 Репозитории " ,
" btn_orgs " : " 🏛 Организации " ,
" btn_back " : " ← Назад к профилю " ,
" profile_header " : " <b>Профиль</b> <a href= \" {url} \" > {username} </a> \n \n " ,
" profile_text " : (
" 👤 Имя: <b> {name} </b> \n "
" 🏷 Логин: <code> {login} </code> \n "
" 📝 Био: {bio} \n "
" 🏢 Компания: {company} \n "
" 📍 Локация: {location} \n "
" 📧 Email: {email} \n "
" 🔗 Сайт: {blog} \n "
" 🐦 Twitter: {twitter} \n "
" 💼 Доступен для найма: {hireable} \n "
" 📊 Тип аккаунта: {type} \n "
" 📦 Публичные репозитории: <b> {repos} </b> \n "
" ⭐ Публичные гисты: <b> {gists} </b> \n "
" 👥 Подписчики: <b> {followers} </b> \n "
" 👣 Подписки: <b> {following} </b> \n "
" 🕒 Создан: <code>{created} </code> "
" 🕐 Создан: <code>{created} </code> \n "
" 🕐 Обновлён: <code> {updated} </code> "
) ,
" activity_header " : " <b>Последняя активность:</b> \n " ,
" activity_commit " : " 🔨 {count} коммит(ов) → <code> {branch} </code> в {repo} " ,
" activity_create " : " ✨ Создан {ref_type} в {repo} " ,
" activity_p r" : " 🔄 {action} PR: {title} " ,
" activity_issue " : " ❗ {action} issue: {title} " ,
" activity_star " : " ⭐ В избранное {repo} " ,
" activity_fork " : " ⑂ Форк в {fork} " ,
" activity_other " : " ⚡ {event} в {repo} " ,
" contrib_header " : " <b>График активности</b> <a href=\" https://github.com/ {username } \" > {username } </a>: \n " ,
" contrib_footer " : " ⬛ = 0, 🟩 = 1+ контрибуций " ,
" activity_header " : " <b>Последняя активность</b> <a href= \" https://github.com/ {username} \" > {username} </a> \n \n " ,
" push_heade r" : " 🔨 Запушил в <code> {branch} </code> → <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" push_no_commits " : " 🔨 Запушил (без деталей) в <code> {branch} </code> → <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" commit_line " : " • <a href= \" {url} \" ><code> {sha} </code></a>: {message} \n " ,
" create_branch " : " ✨ Создал ветку <code> {ref} </code> в <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" create_tag " : " ✨ Создал тег <code> {ref} </code> в <a href=\" https://github.com/ {repo} /releases/tag/ {ref } \" > {repo } </a> \n " ,
" create_repo " : " ✨ Создал репозиторий <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" pr_opened " : " 🔄 Открыл PR <a href= \" {url} \" ># {} {title} </a> \n " ,
" pr_closed " : " 🔄 Закрыл PR <a href= \" {url} \" ># {} {title} </a> \n " ,
" pr_merged " : " 🔄 Замержил PR <a href= \" {url} \" ># {} {title} </a> \n " ,
" issue_opened " : " ❗ Открыл issue <a href= \" {url} \" ># {} {title} </a> \n " ,
" issue_closed " : " ❗ Закрыл issue <a href= \" {url} \" ># {} {title} </a> \n " ,
" star " : " ⭐ Добавил в избранное <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" fork " : " ⑂ Форкнул <a href= \" https://github.com/ {fork} \" > {fork} </a> \n " ,
" other " : " ⚡ {event} в <a href= \" https://github.com/ {repo} \" > {repo} </a> \n " ,
" repos_header " : " <b>Топ репозитории по звёздам</b> <a href= \" https://github.com/ {username} \" > {username} </a> \n \n " ,
" repo_line " : " ⭐ <b> {stars} </b> | <a href= \" {url} \" > {name} </a> — {desc} \n Язык: {lang} | Форков: {forks} \n \n " ,
" orgs_header " : " <b>Организации</b> <a href= \" https://github.com/ {username} \" > {username} </a> \n \n " ,
" org_line " : " • <a href= \" {url} \" > {login} </a> — {desc} \n " ,
" contrib_header " : " <b>График контрибуций (последний год)</b> <a href= \" https://github.com/ {username} \" > {username} </a> \n " ,
" contrib_footer " : " \n ⬛ = 0, 🟩 = 1+ контрибуций " ,
}
def __init__ ( self ) :
self . logger = logging . getLogger ( __name__ )
def github_api ( self , url ) :
async def github_fetch ( self , url , github_api = True ):
headers = {
" User-Agent " : " Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 " ,
" Accept " : " application/vnd.github+json " if github_api else " application/json " ,
" X-GitHub-Api-Version " : " 2022-11-28 " ,
}
req = urllib . request . Request ( url , headers = headers )
try :
with urllib . request . urlopen ( url ) as resp :
return json . loads ( resp . read ( ) . decode ( ) )
with urllib . request . urlopen ( req , timeout = 15 ) as resp :
raw = resp . read ( ) . decode ( " utf-8 " )
return json . loads ( raw ) if raw else { }
except Exception as e :
self . logger . warning ( f " [GitHub API ] { e } " )
return None
self . logger . error ( f " [GitHub] { e } " )
return { " message " : str ( e ) }
def get_username ( self , message ) :
args = message . text . split ( maxsplit = 1 )
return args [ 1 ] if len ( args ) > 1 else None
@loader.command ( doc = " Show GitHub user info " , ru_doc = " Информация о пользователе GitHub " )
async def gh ( self , message ) :
""" Show GitHub user info """
username = self . get_username ( message )
@loader.command ( ru_doc = " { username без @} — Информация о GitHub пользователе " )
async def github ( self , message ) :
""" { username without @} — GitHub user information """
username = utils . get_args_raw ( message )
if not username :
return await message . edit ( self . strings ( " no_username " ) )
await utils . answer ( message , self . strings ( " no_username " ) )
return
data = self . github_api ( f " https://api.github.com/users/ { username } " )
if not data:
return await message . edit ( self . strings ( " user_not_found " ) . format ( username ) )
user_ data = await self . github_fetch ( f " https://api.github.com/users/ { username } " )
if " message " in user_ data:
await utils . answer ( message , self . strings ( " user_not_found " ) . format ( username ) )
return
await message . edi t ( self . strings ( " info_text " ) . format (
name = data . get ( " name " ) or username ,
url = data [ " html_url " ] ,
profile = self . strings ( " profile " ) ,
company = data . get ( " company " , " N/A " ) ,
location = data . get ( " location " , " N/A " ) ,
bio = data . get ( " bio " , " No bio " ) ,
repos = data . get ( " public_repos " , 0 ) ,
followers = data . get ( " followers " , 0 ) ,
following = data . get ( " following " , 0 ) ,
created = data . get ( " created_at " , " " ) [ : 10 ]
) )
hireable = self . strings ( " hireable_yes " ) if user_data . g et( " hireable " ) else self . strings ( " hireable_no " )
@loader.command ( doc = " Show recent GitHub activity " , ru_doc = " Последняя активность GitHub " )
async def gha ( self , messag e) :
""" Show recent GitHub activity """
username = self . get_username ( message )
if not username :
return await message . edi t ( self . strings ( " no_username " ) )
profile_text = (
self . strings ( " profile_header " ) . format ( url = user_data [ " html_url " ] , username = usernam e )
+ self . strings ( " profile_text " ) . format (
name = user_data . get ( " name " ) or self . strings ( " not_specified " ) ,
login = username ,
bio = user_data . g et( " bio " ) or self . strings ( " no_desc " ) ,
company = user_data . get ( " company " ) or self . strings ( " not_specified " ) ,
location = user_data . get ( " location " ) or self . strings ( " not_specified " ) ,
email = user_data . get ( " email " ) or self . strings ( " not_specified " ) ,
blog = user_data . get ( " blog " ) or self . strings ( " not_specified " ) ,
twitter = user_data . get ( " twitter_username " ) or self . strings ( " not_specified " ) ,
hireable = hireable ,
type = user_data . get ( " type " , " User " ) ,
repos = user_data . get ( " public_repos " , 0 ) ,
gists = user_data . get ( " public_gists " , 0 ) ,
followers = user_data . get ( " followers " , 0 ) ,
following = user_data . get ( " following " , 0 ) ,
created = user_data . get ( " created_at " , " " ) [ : 10 ] ,
updated = user_data . get ( " updated_at " , " " ) [ : 10 ] ,
)
+ " \n " + self . strings ( " menu_text " )
)
events = self . github_api ( f " https://api.github.com/users/ { username } /events?per_page=5 " )
await self . inline . form (
message = message ,
text = profile_text ,
reply_markup = [
[ { " text " : self . strings ( " btn_activity " ) , " callback " : self . _activity , " args " : ( username , ) } ] ,
[ { " text " : self . strings ( " btn_contrib " ) , " callback " : self . _contrib , " args " : ( username , ) } , { " text " : self . strings ( " btn_repos " ) , " callback " : self . _repos , " args " : ( username , ) } ] ,
[ { " text " : self . strings ( " btn_orgs " ) , " callback " : self . _orgs , " args " : ( username , ) } ] ,
] ,
ttl = 10 * 60 ,
)
async def _profile ( self , call : InlineCall , username : str ) :
# Этот метод теперь используется только для возврата к профилю
data = await self . github_fetch ( f " https://api.github.com/users/ { username } " )
if " message " in data :
await call . edit ( self . strings ( " api_error " ) . format ( msg = data [ " message " ] ) )
return
hireable = self . strings ( " hireable_yes " ) if data . get ( " hireable " ) else self . strings ( " hireable_no " )
profile_text = (
self . strings ( " profile_header " ) . format ( url = data [ " html_url " ] , username = username )
+ self . strings ( " profile_text " ) . format (
name = data . get ( " name " ) or self . strings ( " not_specified " ) ,
login = username ,
bio = data . get ( " bio " ) or self . strings ( " no_desc " ) ,
company = data . get ( " company " ) or self . strings ( " not_specified " ) ,
location = data . get ( " location " ) or self . strings ( " not_specified " ) ,
email = data . get ( " email " ) or self . strings ( " not_specified " ) ,
blog = data . get ( " blog " ) or self . strings ( " not_specified " ) ,
twitter = data . get ( " twitter_username " ) or self . strings ( " not_specified " ) ,
hireable = hireable ,
type = data . get ( " type " , " User " ) ,
repos = data . get ( " public_repos " , 0 ) ,
gists = data . get ( " public_gists " , 0 ) ,
followers = data . get ( " followers " , 0 ) ,
following = data . get ( " following " , 0 ) ,
created = data . get ( " created_at " , " " ) [ : 10 ] ,
updated = data . get ( " updated_at " , " " ) [ : 10 ] ,
)
+ " \n " + self . strings ( " menu_text " )
)
await call . edit (
text = profile_text ,
reply_markup = [
[ { " text " : self . strings ( " btn_activity " ) , " callback " : self . _activity , " args " : ( username , ) } ] ,
[ { " text " : self . strings ( " btn_contrib " ) , " callback " : self . _contrib , " args " : ( username , ) } , { " text " : self . strings ( " btn_repos " ) , " callback " : self . _repos , " args " : ( username , ) } ] ,
[ { " text " : self . strings ( " btn_orgs " ) , " callback " : self . _orgs , " args " : ( username , ) } ] ,
]
)
async def _activity ( self , call : InlineCall , username : str ) :
events = await self . github_fetch ( f " https://api.github.com/users/ { username } /events?per_page=40 " )
if " message " in events :
await call . edit ( self . strings ( " api_error " ) . format ( msg = events [ " message " ] ) , reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ] )
return
if not events :
return await message . edit ( self . strings ( " no_activity " ) . format ( username ) )
await call . edit ( self . strings ( " no_activity " ) . format ( username = username ) , reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ] )
return
lines = [ ]
for event in events :
lines = [ self . strings ( " activity_header " ) . format ( username = username ) ]
seen_repos = set ( )
for event in events [ : 25 ] :
etype = event [ " type " ]
repo = event [ " repo " ] [ " name " ]
if repo in seen_repos and len ( lines ) > 20 :
continue
payload = event . get ( " payload " , { } )
if etype == " PushEvent " :
branch = re . sub ( r " refs/heads/ " , " " , payload . get ( " ref " , " main " ) )
count = len ( payload . get ( " commits " , [ ] ) )
lines . append ( self . strings ( " activity_commit " ) . format ( count = count , branch = branch , repo = repo ) )
branch = payload . get ( " ref " , " refs/heads/main " ) . replace ( " refs/heads/ " , " " )
commits = payload . get ( " commits " , [ ] )
if commits :
lines . append ( self . strings ( " push_header " ) . format ( branch = branch , repo = repo ) )
for commit in commits [ : 5 ] :
sha = commit [ " sha " ] [ : 7 ]
message = commit [ " message " ] . split ( " \n " ) [ 0 ] [ : 100 ]
if len ( commit [ " message " ] . split ( " \n " ) [ 0 ] ) > 100 :
message + = " ... "
url = f " https://github.com/ { repo } /commit/ { commit [ ' sha ' ] } "
lines . append ( self . strings ( " commit_line " ) . format ( url = url , sha = sha , message = message ) )
if len ( commits ) > 5 :
lines . append ( self . strings ( " more_commits " ) . format ( len ( commits ) - 5 ) )
else :
lines . append ( self . strings ( " push_no_commits " ) . format ( branch = branch , repo = repo ) )
seen_repos . add ( repo )
elif etype == " CreateEvent " :
lines . append ( self . strings ( " activity_create " ) . format ( ref_type= payload . get ( " ref_type " ) , repo = repo ) )
ref_type = payload . get ( " ref_type " )
ref = payload . get ( " ref " ) or " "
if ref_type == " branch " :
lines . append ( self . strings ( " create_branch " ) . format ( ref = ref , repo = repo ) )
elif ref_type == " tag " :
lines . append ( self . strings ( " create_tag " ) . format ( ref = ref , repo = repo ) )
elif ref_type == " repository " :
lines . append ( self . strings ( " create_repo " ) . format ( repo = repo ) )
elif etype == " PullRequestEvent " :
pr = payload . get ( " pull_request " , { } )
lines . append ( self . strings ( " activity_pr " ) . format ( action = payload . get ( " action " ) , title = pr . get ( " title " ) ) )
number = pr . get ( " number " , " ? " )
title = pr . get ( " title " ) or self . strings ( " no_title " )
url = pr . get ( " html_url " ) or f " https://github.com/ { repo } "
action = payload . get ( " action " )
if action == " closed " and pr . get ( " merged " ) :
lines . append ( self . strings ( " pr_merged " ) . format ( url = url , number = number , title = title ) )
elif action == " opened " :
lines . append ( self . strings ( " pr_opened " ) . format ( url = url , number = number , title = title ) )
elif action == " closed " :
lines . append ( self . strings ( " pr_closed " ) . format ( url = url , number = number , title = title ) )
elif etype == " IssuesEvent " :
issue = payload . get ( " issue " , { } )
lines . append ( self . strings ( " activity_issue " ) . format ( action = payload . get ( " action " ) , title = issue . get ( " title " ) ) )
number = issue . get ( " number " , " ? " )
title = issue . get ( " title " ) or self . strings ( " no_title " )
url = issue . get ( " html_url " ) or f " https://github.com/ { repo } "
action = payload . get ( " action " )
if action == " opened " :
lines . append ( self . strings ( " issue_opened " ) . format ( url = url , number = number , title = title ) )
elif action == " closed " :
lines . append ( self . strings ( " issue_closed " ) . format ( url = url , number = number , title = title ) )
elif etype == " WatchEvent " :
lines . append ( self . strings ( " activity_ star" ) . format ( repo = repo ) )
lines . append ( self . strings ( " star " ) . format ( repo = repo ) )
elif etype == " ForkEvent " :
lines . append ( self . strings ( " activity_fork " ) . format ( fork= payload . get ( " forkee " , { } ) . get ( " full_name " ) ) )
fork = payload . get ( " forkee " , { } ) . get ( " full_name " , " unknown " )
lines . append ( self . strings ( " fork " ) . format ( fork = fork ) )
else :
lines . append ( self . strings ( " activity_other " ) . format ( event = etype , repo = repo ) )
event_name = etype . replace ( " Event " , " " )
lines . append ( self . strings ( " other " ) . format ( event = event_name , repo = repo ) )
await message . edit ( self . strings ( " activity_header " ) + " \n " . join ( lines ) )
await call . edit (
text = " " . join ( lines ) ,
reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ]
)
@loader.command ( doc = " Show GitHub contribution graph " , ru_doc = " Показать график контрибов GitHub " )
async def ghc ( self , message ) :
""" Show GitHub contribution graph """
username = self . get_username ( message )
if not username :
return await message . edit ( self . strings ( " no_username " ) )
async def _repos ( self , call : InlineCall , username : str ) :
repos = await self . github_fetch ( f " https://api.github.com/users/ { username } /repos?sort=stars&per_page=10 " )
if " message " in repos :
await call . edit ( self . strings ( " api_error " ) . format ( msg = repos [ " message " ] ) , reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ] )
return
if not repos :
await call . edit ( self . strings ( " no_repos " ) , reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ] )
return
data = self . github_api ( f " https://github-contributions-api.deno.dev/ { username } .json " )
contribs = data . get ( " contributions " ) if data else None
lines = [ self . strings ( " repos_header " ) . format ( username = username ) ]
for repo in repos [ : 10 ] :
lines . append ( self . strings ( " repo_line " ) . format (
stars = repo . get ( " stargazers_count " , 0 ) ,
url = repo [ " html_url " ] ,
name = repo [ " name " ] ,
desc = repo . get ( " description " ) or self . strings ( " no_desc " ) ,
lang = repo . get ( " language " ) or self . strings ( " not_specified " ) ,
forks = repo . get ( " forks_count " , 0 ) ,
) )
if not isinstance ( contribs , list ) :
return await message . edit ( self . strings ( " no_contrib " ) . format ( username ) )
await call . edit (
text = " " . join ( lines ) ,
reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ]
)
async def _orgs ( self , call : InlineCall , username : str ) :
orgs = await self . github_fetch ( f " https://api.github.com/users/ { username } /orgs " )
if " message " in orgs :
await call . edit ( self . strings ( " api_error " ) . format ( msg = orgs [ " message " ] ) , reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ] )
return
if not orgs :
await call . edit ( self . strings ( " no_orgs " ) , reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ] )
return
lines = [ self . strings ( " orgs_header " ) . format ( username = username ) ]
for org in orgs :
lines . append ( self . strings ( " org_line " ) . format (
url = f " https://github.com/ { org [ ' login ' ] } " ,
login = org [ " login " ] ,
desc = org . get ( " description " ) or self . strings ( " no_desc " ) ,
) )
await call . edit (
text = " " . join ( lines ) ,
reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ]
)
async def _contrib ( self , call : InlineCall , username : str ) :
data = await self . github_fetch ( f " https://github-contributions-api.deno.dev/ { username } .json " , github_api = False )
if not data or not data . get ( " contributions " ) :
await call . edit ( self . strings ( " no_contrib " ) , reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ] )
return
raw_days = [ ]
for week in data . get ( " contributions " , [ ] ) :
if isinstance ( week , list ) :
raw_days . extend ( [ day for day in week if isinstance ( day , dict ) ] )
today = datetime . utcnow ( ) . date ( )
start = today - timedelta ( days = 90 )
matrix = [ [ " ⬛ " for _ in range ( 13 ) ] for _ in range ( 7 ) ]
weeks_count = 53
days_back = weeks_count * 7 + 7
start = today - timedelta ( days = days_back )
for entry in contribs :
matrix = [ [ " ⬛ " for _ in range ( weeks_ cou nt) ] for _ in range ( 7 ) ]
for entry in raw_days :
date_str = entry . get ( " date " )
if not date_str :
continue
try :
date = datetime . strptime ( entry [ " date " ] , " % Y- % m- %d " ) . date ( )
if not ( start < = date < = today ) :
date = datetime . strptime ( date_str , " % Y- % m- %d " ) . date ( )
if date < start or date > today :
continue
day = ( date . weekday ( ) + 1 ) % 7 # Sunday= 0
week = ( date - start ) . days / / 7
if entry . get ( " contributionCount " , 0 ) > 0 :
matrix [ day ] [ week ] = " 🟩 "
excep t:
count = entry . get ( " contributionCount " ) or entry . get ( " count " , 0 ) or 0
if count > 0 :
day_idx = ( date . weekday ( ) + 1 ) % 7
week_idx = ( date - start ) . days / / 7
if week_idx < weeks_coun t :
matrix [ day_idx ] [ week_idx ] = " 🟩 "
except Exception :
continue
days = [ " Sun " , " Mon " , " Tue " , " Wed " , " Thu " , " Fri " , " Sat " ]
graph = " \n " . join ( f " { days [ i ] } { ' ' . join ( matrix [ i ] ) } " for i in range ( 7 ) )
days_labels = [ " Sun " , " Mon " , " Tue " , " Wed " , " Thu " , " Fri " , " Sat " ]
graph = " \n " . join ( f " { days_labels [ i ] } { ' ' . join ( matrix [ i ] ) } " for i in range ( 7 ) )
await message . edit (
self . strings ( " contrib_header " ) . format ( username = username )
+ f " <pre> { graph } </pre> \n "
+ self . strings ( " contrib_footer " )
await call . edit (
text = self . strings ( " contrib_header " ) . format ( username = username )
+ f " <pre> { graph } </pre> "
+ self . strings ( " contrib_footer " ) ,
reply_markup = [ [ { " text " : self . strings ( " btn_back " ) , " callback " : self . _profile , " args " : ( username , ) } ] ]
)