forked from wiz0u/WTelegramClient
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathProgram_Heroku.cs
152 lines (141 loc) · 7.61 KB
/
Program_Heroku.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
using Npgsql;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using TL;
// This is an example userbot designed to run on a Heroku account with a PostgreSQL database for session storage
// This userbot simply answer "Pong" when someone sends him a "Ping" private message (or in Saved Messages)
// To use/install/deploy this userbot, follow the steps at the end of this file
// When run locally, close the window or type ALT-F4 to exit cleanly and save session (similar to Heroku SIGTERM)
namespace WTelegramClientTest
{
static class Program_Heroku
{
static WTelegram.Client Client;
static User My;
static readonly Dictionary<long, User> Users = new();
static readonly Dictionary<long, ChatBase> Chats = new();
// See steps at the end of this file to setup required Environment variables
static async Task Main(string[] _)
{
var exit = new SemaphoreSlim(0);
AppDomain.CurrentDomain.ProcessExit += (s, e) => exit.Release(); // detect SIGTERM to exit gracefully
var store = new PostgreStore(Environment.GetEnvironmentVariable("DATABASE_URL"), Environment.GetEnvironmentVariable("SESSION_NAME"));
// if DB does not contain a session yet, client will be run in interactive mode
Client = new WTelegram.Client(store.Length == 0 ? null : Environment.GetEnvironmentVariable, store);
using (Client)
{
Client.OnUpdate += Client_OnUpdate;
My = await Client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})");
var dialogs = await Client.Messages_GetAllDialogs();
dialogs.CollectUsersChats(Users, Chats);
await exit.WaitAsync();
}
}
private static async Task Client_OnUpdate(IObject arg)
{
if (arg is not UpdatesBase updates) return;
updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList)
{
Console.WriteLine(update.GetType().Name);
if (update is UpdateNewMessage { message: Message { peer_id: PeerUser { user_id: var user_id } } msg }) // private message
if (!msg.flags.HasFlag(Message.Flags.out_)) // ignore our own outgoing messages
if (Users.TryGetValue(user_id, out var user))
{
Console.WriteLine($"New message from {user}: {msg.message}");
if (msg.message.Equals("Ping", StringComparison.OrdinalIgnoreCase))
await Client.SendMessageAsync(user, "Pong");
}
}
}
}
#region PostgreSQL session store
class PostgreStore : Stream
{
private readonly NpgsqlConnection _sql;
private readonly string _sessionName;
private byte[] _data;
private int _dataLen;
private DateTime _lastWrite;
private Task _delayedWrite;
/// <param name="databaseUrl">Heroku DB URL of the form "postgres://user:password@host:port/database"</param>
/// <param name="sessionName">Entry name for the session data in the WTelegram_sessions table (default: "Heroku")</param>
public PostgreStore(string databaseUrl, string sessionName = null)
{
_sessionName = sessionName ?? "Heroku";
var parts = databaseUrl.Split(':', '/', '@');
_sql = new NpgsqlConnection($"User ID={parts[3]};Password={parts[4]};Host={parts[5]};Port={parts[6]};Database={parts[7]};Pooling=true;SSL Mode=Require;Trust Server Certificate=True;");
_sql.Open();
using (var create = new NpgsqlCommand($"CREATE TABLE IF NOT EXISTS WTelegram_sessions (name text NOT NULL PRIMARY KEY, data bytea)", _sql))
create.ExecuteNonQuery();
using var cmd = new NpgsqlCommand($"SELECT data FROM WTelegram_sessions WHERE name = '{_sessionName}'", _sql);
using var rdr = cmd.ExecuteReader();
if (rdr.Read())
_dataLen = (_data = rdr[0] as byte[]).Length;
}
protected override void Dispose(bool disposing)
{
_delayedWrite?.Wait();
_sql.Dispose();
}
public override int Read(byte[] buffer, int offset, int count)
{
Array.Copy(_data, 0, buffer, offset, count);
return count;
}
public override void Write(byte[] buffer, int offset, int count) // Write call and buffer modifications are done within a lock()
{
_data = buffer; _dataLen = count;
if (_delayedWrite != null) return;
var left = 1000 - (int)(DateTime.UtcNow - _lastWrite).TotalMilliseconds;
if (left < 0)
{
using var cmd = new NpgsqlCommand($"INSERT INTO WTelegram_sessions (name, data) VALUES ('{_sessionName}', @data) ON CONFLICT (name) DO UPDATE SET data = EXCLUDED.data", _sql);
cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]);
cmd.ExecuteNonQuery();
_lastWrite = DateTime.UtcNow;
}
else // delay writings for a full second
_delayedWrite = Task.Delay(left).ContinueWith(t => { lock (this) { _delayedWrite = null; Write(_data, 0, _dataLen); } });
}
public override long Length => _dataLen;
public override long Position { get => 0; set { } }
public override bool CanSeek => false;
public override bool CanRead => true;
public override bool CanWrite => true;
public override long Seek(long offset, SeekOrigin origin) => 0;
public override void SetLength(long value) { }
public override void Flush() { }
}
#endregion
}
/******************************************************************************************************************************
HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT:
- From your Heroku.com account dashboard, create a new app
- Navigate to the app Resources and add the add-on "Heroku Postgres"
- Navigate to the app Settings, click Reveal Config Vars and save the Heroku git URL and the value of DATABASE_URL
- Add a new var named "api_hash" with your api hash obtained from https://my.telegram.org/apps
- Add a new var named "phone_number" with the phone_number of the user this userbot will manage
- Scroll down to Buildpacks and add this URL: https://github.com/jincod/dotnetcore-buildpack.git
- In Visual Studio, Clone the Heroku git repository and add some standard .gitignore .gitattributes files
- In this repository folder, create a new .NET Console project with this Program.cs file
- Add these Nuget packages: WTelegramClient and Npgsql
- In Project properties > Debug > Environment variables, configure the same values for DATABASE_URL, api_hash, phone_number
- Run the project in Visual Studio. The first time, it should ask you interactively for elements to complete the connection
- On the following runs, the PostgreSQL database contains the session data and it should connect automatically
- You can test the userbot by sending him "Ping" in private message (or saved messages). It should respond with "Pong"
- You can now commit & push your git sources to Heroku, they will be compiled and deployed/run as a web app
- The userbot should work fine for 1 minute, but it will be taken down because it is not a web app. Let's fix that
- Navigate to the app Resources, copy the line starting with: web cd $HOME/.......
- Back in Visual Studio (or Explorer), at the root of the repository create a Procfile text file (without extension)
- Paste inside the line you copied, and replace the initial "web" with "worker:" (don't forget the colon)
- Commit and push the Git changes to Heroku. Wait until the deployment is complete.
- Verify that the Resources "web" line has changed to "worker" and is enabled (use the pencil icon if necessary)
- Now your userbot should be running 24/7.
- To prevent AUTH_KEY_DUPLICATED issues, set a SESSION_NAME env variable in your local VS project with a value like "PC"
DISCLAIMER: I'm not affiliated nor expert with Heroku, so if you have any problem with the above I might not be able to help
******************************************************************************************************************************/