Skip to content

Commit a84cb9c

Browse files
committed
Autoupdate functions
1 parent 29747b7 commit a84cb9c

File tree

10 files changed

+493
-5
lines changed

10 files changed

+493
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
77
however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section.
88

9-
# v3.7.0-dev16
9+
# v3.7.0-dev17
1010

1111
### Added
1212

@@ -28,6 +28,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s
2828
- Added `data_collection` to specify if bot metadata should be collected by Modmail developers.
2929
- Added `?autotrigger`, `use_regex_autotrigger` config to specify keywords to trigger commands. ([GH #130](https://github.com/kyb3r/modmail/issues/130), [GH #649](https://github.com/kyb3r/modmail/issues/649))
3030
- Added `?note persistent` that creates notes that are persistent for a user. ([GH #2842](https://github.com/kyb3r/modmail/issues/2842), [PR #2878](https://github.com/kyb3r/modmail/pull/2878))
31+
- Autoupdates and `?update` which was removed in v3.0.0
3132

3233
### Fixed
3334

bot.py

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
__version__ = "3.7.0-dev16"
1+
__version__ = "3.7.0-dev17"
22

33

44
import asyncio
55
import copy
66
import logging
77
import os
88
import re
9+
import subprocess
910
import sys
1011
import typing
1112
from datetime import datetime
@@ -31,9 +32,17 @@
3132
pass
3233

3334
from core import checks
35+
from core.changelog import Changelog
3436
from core.clients import ApiClient, MongoDBClient, PluginDatabaseClient
3537
from core.config import ConfigManager
36-
from core.models import DMDisabled, PermissionLevel, SafeFormatter, configure_logging, getLogger
38+
from core.models import (
39+
DMDisabled,
40+
HostingMethod,
41+
PermissionLevel,
42+
SafeFormatter,
43+
configure_logging,
44+
getLogger,
45+
)
3746
from core.thread import ThreadManager
3847
from core.time import human_timedelta
3948
from core.utils import human_join, match_title, normalize_alias
@@ -58,6 +67,7 @@ def __init__(self):
5867
self._session = None
5968
self._api = None
6069
self.metadata_loop = None
70+
self.autoupdate_loop = None
6171
self.formatter = SafeFormatter()
6272
self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"]
6373
self._connected = asyncio.Event()
@@ -88,6 +98,21 @@ def uptime(self) -> str:
8898

8999
return self.formatter.format(fmt, d=days, h=hours, m=minutes, s=seconds)
90100

101+
@property
102+
def hosting_method(self) -> HostingMethod:
103+
# use enums
104+
if ".heroku" in os.environ.get("PYTHONHOME", ""):
105+
return HostingMethod.HEROKU
106+
107+
if os.environ.get("pm_id"):
108+
return HostingMethod.PM2
109+
110+
return HostingMethod.OTHER
111+
112+
@property
113+
def is_pm2(self) -> bool:
114+
return ".heroku" in os.environ.get("PYTHONHOME", "")
115+
91116
def startup(self):
92117
logger.line()
93118
if os.name != "nt":
@@ -494,6 +519,12 @@ async def on_ready(self):
494519
self.metadata_loop.before_loop(self.before_post_metadata)
495520
self.metadata_loop.start()
496521

522+
self.autoupdate_loop = tasks.Loop(
523+
self.autoupdate, seconds=0, minutes=0, hours=1, count=None, reconnect=True, loop=None
524+
)
525+
self.autoupdate_loop.before_loop(self.before_autoupdate)
526+
self.autoupdate_loop.start()
527+
497528
other_guilds = [
498529
guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild}
499530
]
@@ -1398,6 +1429,81 @@ async def before_post_metadata(self):
13981429
if not self.guild:
13991430
self.metadata_loop.cancel()
14001431

1432+
async def autoupdate(self):
1433+
changelog = await Changelog.from_url(self)
1434+
latest = changelog.latest_version
1435+
1436+
if self.version < parse_version(latest.version):
1437+
if self.hosting_method == HostingMethod.HEROKU:
1438+
data = await self.api.update_repository()
1439+
1440+
embed = discord.Embed(color=self.main_color)
1441+
1442+
commit_data = data["data"]
1443+
user = data["user"]
1444+
embed.set_author(
1445+
name=user["username"] + " - Updating Bot",
1446+
icon_url=user["avatar_url"],
1447+
url=user["url"],
1448+
)
1449+
1450+
embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}")
1451+
1452+
embed.description = latest.description
1453+
for name, value in latest.fields.items():
1454+
embed.add_field(name=name, value=value)
1455+
1456+
if commit_data:
1457+
message = commit_data["commit"]["message"]
1458+
html_url = commit_data["html_url"]
1459+
short_sha = commit_data["sha"][:6]
1460+
embed.add_field(
1461+
name="Merge Commit",
1462+
value=f"[`{short_sha}`]({html_url}) " f"{message} - {user['username']}",
1463+
)
1464+
logger.info("Bot has been updated.")
1465+
channel = self.log_channel
1466+
await channel.send(embed=embed)
1467+
else:
1468+
command = "git pull"
1469+
1470+
cmd = subprocess.run(
1471+
command,
1472+
cwd=os.getcwd(),
1473+
stderr=subprocess.PIPE,
1474+
stdout=subprocess.PIPE,
1475+
shell=True,
1476+
)
1477+
res = cmd.stdout.decode("utf-8").strip()
1478+
1479+
if res != "Already up to date.":
1480+
logger.info("Bot has been updated.")
1481+
channel = self.log_channel
1482+
if self.hosting_method == HostingMethod.PM2:
1483+
embed = discord.Embed(title="Bot has been updated", color=self.main_color)
1484+
await channel.send(embed=embed)
1485+
else:
1486+
embed = discord.Embed(
1487+
title="Bot has been updated and is logging out.",
1488+
description="If you do not have an auto-restart setup, please manually start the bot.",
1489+
color=self.main_color,
1490+
)
1491+
await channel.send(embed=embed)
1492+
await self.logout()
1493+
1494+
async def before_autoupdate(self):
1495+
await self.wait_for_connected()
1496+
logger.debug("Starting autoupdate loop")
1497+
1498+
if self.config.get("disable_autoupdates"):
1499+
logger.warning("Autoupdates disabled.")
1500+
self.autoupdate_loop.cancel()
1501+
1502+
if not self.config.get("github_token"):
1503+
logger.warning("GitHub access token not found.")
1504+
logger.warning("Autoupdates disabled.")
1505+
self.autoupdate_loop.cancel()
1506+
14011507

14021508
def main():
14031509
try:

cogs/modmail.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,6 +1535,15 @@ async def isenable(self, ctx):
15351535

15361536
return await ctx.send(embed=embed)
15371537

1538+
@commands.command(usage="[after] [close message]")
1539+
@checks.has_permissions(PermissionLevel.SUPPORTER)
1540+
@checks.thread_only()
1541+
async def adduser(self, ctx, *, member: discord.Member = None):
1542+
await ctx.thread.add_user(member)
1543+
1544+
sent_emoji, _ = await self.bot.retrieve_emoji()
1545+
await self.bot.add_reaction(ctx.message, sent_emoji)
1546+
15381547

15391548
def setup(bot):
15401549
bot.add_cog(Modmail(bot))

cogs/utility.py

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from io import BytesIO, StringIO
1111
from itertools import takewhile, zip_longest
1212
from json import JSONDecodeError, loads
13+
import subprocess
1314
from textwrap import indent
1415
from types import SimpleNamespace
1516
from typing import Union
@@ -24,12 +25,13 @@
2425
from core import checks, utils
2526
from core.changelog import Changelog
2627
from core.models import (
28+
HostingMethod,
2729
InvalidConfigError,
2830
PermissionLevel,
29-
SimilarCategoryConverter,
3031
UnseenFormatter,
3132
getLogger,
3233
)
34+
from core.utils import trigger_typing
3335
from core.paginator import EmbedPaginatorSession, MessagePaginatorSession
3436

3537

@@ -1825,6 +1827,122 @@ async def autotrigger_list(self, ctx):
18251827

18261828
await EmbedPaginatorSession(ctx, *embeds).run()
18271829

1830+
@commands.command()
1831+
@checks.has_permissions(PermissionLevel.OWNER)
1832+
@checks.github_token_required()
1833+
@trigger_typing
1834+
async def github(self, ctx):
1835+
"""Shows the GitHub user your Github_Token is linked to."""
1836+
data = await self.bot.api.get_user_info()
1837+
1838+
embed = discord.Embed(
1839+
title="GitHub", description="Current User", color=self.bot.main_color
1840+
)
1841+
user = data["user"]
1842+
embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"])
1843+
embed.set_thumbnail(url=user["avatar_url"])
1844+
await ctx.send(embed=embed)
1845+
1846+
@commands.command()
1847+
@checks.has_permissions(PermissionLevel.OWNER)
1848+
@checks.github_token_required(ignore_if_not_heroku=True)
1849+
@trigger_typing
1850+
async def update(self, ctx, *, flag: str = ""):
1851+
"""
1852+
Update Modmail.
1853+
This only works for PM2 or Heroku users who have configured their bot for updates.
1854+
To stay up-to-date with the latest commit
1855+
from GitHub, specify "force" as the flag.
1856+
"""
1857+
1858+
changelog = await Changelog.from_url(self.bot)
1859+
latest = changelog.latest_version
1860+
1861+
desc = (
1862+
f"The latest version is [`{self.bot.version}`]"
1863+
"(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)"
1864+
)
1865+
1866+
if self.bot.version >= parse_version(latest.version) and flag.lower() != "force":
1867+
embed = discord.Embed(
1868+
title="Already up to date", description=desc, color=self.bot.main_color
1869+
)
1870+
1871+
data = await self.bot.api.get_user_info()
1872+
if not data.get("error"):
1873+
user = data["user"]
1874+
embed.set_author(
1875+
name=user["username"], icon_url=user["avatar_url"], url=user["url"]
1876+
)
1877+
await ctx.send(embed=embed)
1878+
else:
1879+
if self.bot.hosting_method == HostingMethod.HEROKU:
1880+
data = await self.bot.api.update_repository()
1881+
1882+
commit_data = data["data"]
1883+
user = data["user"]
1884+
1885+
if commit_data:
1886+
embed = discord.Embed(color=self.bot.main_color)
1887+
1888+
embed.set_footer(
1889+
text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}"
1890+
)
1891+
1892+
embed.set_author(
1893+
name=user["username"] + " - Updating bot",
1894+
icon_url=user["avatar_url"],
1895+
url=user["url"],
1896+
)
1897+
1898+
embed.description = latest.description
1899+
for name, value in latest.fields.items():
1900+
embed.add_field(name=name, value=value)
1901+
# message = commit_data['commit']['message']
1902+
html_url = commit_data["html_url"]
1903+
short_sha = commit_data["sha"][:6]
1904+
embed.add_field(name="Merge Commit", value=f"[`{short_sha}`]({html_url})")
1905+
else:
1906+
embed = discord.Embed(
1907+
title="Already up to date with master repository.",
1908+
description="No further updates required",
1909+
color=self.bot.main_color,
1910+
)
1911+
embed.set_author(
1912+
name=user["username"], icon_url=user["avatar_url"], url=user["url"]
1913+
)
1914+
await ctx.send(embed=embed)
1915+
else:
1916+
command = "git pull"
1917+
1918+
cmd = subprocess.run(
1919+
command,
1920+
cwd=os.getcwd(),
1921+
stderr=subprocess.PIPE,
1922+
stdout=subprocess.PIPE,
1923+
shell=True,
1924+
)
1925+
res = cmd.stdout.decode("utf-8").strip()
1926+
1927+
if res != "Already up to date.":
1928+
logger.info("Bot has been updated.")
1929+
if self.bot.hosting_method == HostingMethod.PM2:
1930+
embed = discord.Embed(
1931+
title="Bot has been updated", color=self.bot.main_color
1932+
)
1933+
await ctx.send(embed=embed)
1934+
else:
1935+
embed = discord.Embed(
1936+
title="Bot has been updated and is logging out.",
1937+
description="If you do not have an auto-restart setup, please manually start the bot.",
1938+
color=self.bot.main_color,
1939+
)
1940+
await ctx.send(embed=embed)
1941+
await self.bot.logout()
1942+
else:
1943+
embed = discord.Embed(title="Already up to date.", color=self.bot.main_color,)
1944+
await ctx.send(embed=embed)
1945+
18281946
@commands.command(hidden=True, name="eval")
18291947
@checks.has_permissions(PermissionLevel.OWNER)
18301948
async def eval_(self, ctx, *, body: str):

core/checks.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from discord.ext import commands
22

3-
from core.models import PermissionLevel, getLogger
3+
from core.models import HostingMethod, PermissionLevel, getLogger
44

55
logger = getLogger(__name__)
66

@@ -100,3 +100,23 @@ async def predicate(ctx):
100100

101101
predicate.fail_msg = "This is not a Modmail thread."
102102
return commands.check(predicate)
103+
104+
105+
def github_token_required(ignore_if_not_heroku=False):
106+
"""
107+
A decorator that ensures github token
108+
is set
109+
"""
110+
111+
async def predicate(ctx):
112+
if ignore_if_not_heroku and ctx.bot.hosting_method != HostingMethod.HEROKU:
113+
return True
114+
else:
115+
return ctx.bot.config.get("github_token")
116+
117+
predicate.fail_msg = (
118+
"You can only use this command if you have a "
119+
"configured `GITHUB_TOKEN`. Get a "
120+
"personal access token from developer settings."
121+
)
122+
return commands.check(predicate)

0 commit comments

Comments
 (0)